/* var_info.h */

#ifndef DODO_VAR_INFO_H
#define DODO_VAR_INFO_H

#include <list>
#include <string>
#include <fstream>
#include <boost/cstdint.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include "defines.h"

#ifndef NO_NEED_TO_SWAP_ENDIAN
#include <dodo_utils/swap_endian.h>
#endif // NO_NEED_TO_SWAP_ENDIAN

namespace dodomet
{
namespace mm5
{
// forward declaration
class MM5_File;
class Sub_Header;

// forward declaration
class Infolet;

/// class Infolet's implement class.
class Infolet_Impl
{
	friend class Infolet;
private:
	Infolet_Impl();
	Infolet_Impl(const MM5_File& mm5_file, const Sub_Header& sub_header);
	Infolet_Impl(const std::string& filename, const boost::posix_time::ptime& start_time, const Sub_Header& sub_header);
	Infolet_Impl(const Infolet_Impl& orig);
private:
	std::string filename() const;
	boost::posix_time::ptime start_time() const;
	boost::posix_time::time_duration xtime() const;
	fortran_real xtime_real() const;
	std::streampos field_pos() const;
private:
	std::string filename_;
	boost::posix_time::ptime start_time_;
	boost::posix_time::time_duration xtime_;
	std::streampos field_pos_;
private:
	int use_count_;
};

/// Infolet is a record for one time_step of a variable.
/**
 * it contains the source mm5 file name of the record,
 * the model start time and forecast time (i.e. xtime), etc.
 * Var_Info holds a list of Infolet.
 */
class Infolet
{
public:
	Infolet();
	Infolet(const MM5_File& mm5_file, const Sub_Header& sub_header);
	Infolet(const std::string& filename, const boost::posix_time::ptime& start_time, const Sub_Header& sub_header);
	Infolet(const Infolet& orig);
	~Infolet();
	Infolet& operator= (const Infolet& orig);

private:
	void on_write();

public:
	/// compares 2 Infolet objects.
	/**
	 * when comparing 2 Infolet objects, compare their model start time first,
	 * if start_time equals, compare their forecast time then.
	 */
	bool operator< (const Infolet& info) const;

	/// compares 2 Infolet objects.
	/** if model start time and forecast time equals,
	 * 2 Infolet objects are considered equal.
	 */
	bool operator== (const Infolet& info) const;

public:
	/// returns the name of the file from which this record comes.
	std::string filename() const {return impl_->filename();}

	/// returns model start time.
	boost::posix_time::ptime start_time() const {return impl_->start_time();}

	/// returns model forecast duration.
	boost::posix_time::time_duration xtime() const {return impl_->xtime();}

	/// returns model forecast duration in minutes.
	fortran_real xtime_real() const {return impl_->xtime_real();}

	/// returns current time of the record.
	boost::posix_time::ptime current_time() const {return start_time() + xtime();}

	/// you won't use this interface directly.
	/**
	 * it's for Var_Info to read and write files.
	 * returns position of the field in the file.
	 */
	std::streampos field_pos() const {return impl_->field_pos();}
private:
	Infolet_Impl *impl_;
};

class Var_Info;

/// class Var_Info's implement class.
class Var_Info_Impl
{
public:
	friend class Var_Info;
protected:
	Var_Info_Impl();
	Var_Info_Impl(const Sub_Header& sub_header);
	Var_Info_Impl(const Var_Info_Impl& orig);
	void init(const Sub_Header& sub_header);
protected:
//	typedefs

	typedef std::list<Infolet>::const_iterator const_iterator;
	typedef std::list<Infolet>::iterator iterator;
//	queries
	std::string name() const;
	std::string unit() const;
	std::string description() const;

	int ndim() const;
	int start_index(int dim) const;
	int end_index(int dim) const;
	int dim_size(int dim) const;

	int ntime_steps() const;
	int field_size() const;
	const_iterator begin() const;
	const_iterator end() const;

	std::string ordering_name() const;
	int ordering() const;

//	operation
	void add_record(const Infolet& infolet);

	template<class UnPred>
	void remove_if(UnPred pred);

	void keep_only_one_record();

private:
	int ndim_;
	int start_index_[4];
	int end_index_[4];
	std::string name_;
	std::string unit_;
	std::string description_;
	int staggering_;
	int ordering_;

	std::list<Infolet> field_pos_;

private:
	bool inited_;
	int use_count_;
};

/// Var_Info holds information of a variable of MM5 model files.
/**
 * It holds a list made up with Infolet s, i.e. the records of each time step.
 */
class Var_Info
{
public:
	Var_Info();

	/// initializing with Sub_Header. not for user.
	Var_Info(const Sub_Header& sub_header);

	Var_Info(const Var_Info& orig);
	~Var_Info();
	Var_Info& operator= (const Var_Info& orig);
private:
	/// helper function for deep copy when making changes to a Var_Info object
	void on_write();
public:
	/// initializing with Sub_Header. not for user.
	void init(const Sub_Header& sub_header);
public:
//	typedefs

	/// const_iterator to an Infolet.
	typedef Var_Info_Impl::const_iterator const_iterator;

	/// iterator to an Infolet.
	typedef Var_Info_Impl::iterator iterator;

//	queries

	/// returns variable name.
	std::string name() const;

	/// returns unit of variable.
	std::string unit() const;

	/// returns description of variable.
	std::string description() const;

	/// returns dimension number of the variable.
	int ndim() const;

	/// returns the start index of that dimension.
	/**
	 * usually this should be 1, for Fortran's sake.
	 * parameter dim mean the n_th dimension.
	 * dim should be 0, 1, 2.
	 */
	int start_index(int dim) const;

	/// returns the end index of that dimension.
	/** usually this should be IX, JX, KX.
	 * parameter dim mean the n_th dimension.
	 * dim should be 0, 1, 2.
	 */
	int end_index(int dim) const;

	/// the size of that dimension.
	/**
	 * dim should be 0, 1, 2.
	 */
	int dim_size(int dim) const;

public:
	/// returns number of time steps for this variable.
	int ntime_steps() const;

	/// returns const_iterator to the begin of record list.
	const_iterator begin() const;

	/// returns const_iterator to the end of record list.
	/**
	 * remember end means the next position to the last list element.
	 */
	const_iterator end() const;

	/// returns the total size that the variable would cost.
	/**
	 * you need a container at least that big for reading and writing.
	 * it equals field_size() * ntime_steps();
	 */
	int total_size() const;

	/// returns the size of the variable field.
	/**
	 * it equals dim_size(0) * dim_size(1) * dim_size(2).
	 */
	int field_size() const;

	/// returns data ordering name of MM5 file, such as "YX", "YXS", etc.
	std::string ordering_name() const;

	/// returns an index that represents data ordering.
	/**
	 * see namespace dodomet::mm5::ordering_names in defines.h for more infomation.
	 */
	int ordering() const;

//	operation

	/// adding a new record.
	/**
	 * there won't be duplicate records.
	 * it may throw a exceptions::object_uninitialized exception if it hasn't been initialised.
	 * This should not happen if you are using Var_Info objects which are generated by the library components.
	 */
	void add_record(const Infolet& infolet);

	/// merging another Var_Info object into it.
	/**
	 * there won't be duplicate records.
	 */
	void merge(const Var_Info& info);

	/// remove specific records which meet the condition.
	/**
	 * pred is a function or functor that takes an Infolet as parameter and returns a bool.
	 */
	template<class UnPred>
	void remove_if(UnPred pred);

	/// keep only one record(the first one), other record will be removed.
	void keep_only_one_record();

	/// fill a container with the variable data.
	/**
	 * returns whether successful.
	 * the container should be at least as big as total_size();
	 * after filling, the arrangement order of the data will be fortran style.
	 * container_begin is the begin() of the container,
	 * for C's raw array type, it's the address of the array's first element.
	 * It may throw a exceptions::cannot_open_file exception if the file cannot be opened.
	 */
	template<class Iter>
	bool fill_data(Iter container_begin) const;

	/// fill a container with the specific infolet's variable data, i.e. only fill with one-time-step data reference by the given infolet.
	/**
	 * returns whether successful.
	 * the container should be at least as big as field_size();
	 * after filling, the arrangement order of the data will be fortran style.
	 * container_begin is the begin() of the container,
	 * for C's raw array type, it's the address of the array's first element.
	 * It may throw a exceptions::cannot_open_file exception if the file cannot be opened.
	 */
	template<class Iter>
	bool fill_data(Iter container_begin, const Infolet& infolet) const;

	/// write the variable data in a container back to the original MM5 model file.
	/**
	 * returns whether successful.
	 * the container should be at least as big as total_size();
	 * and the element arrangement order should be in fortran style.
	 * container_begin is the begin() of the container,
	 * for C's raw array type, it's the address of the array's first element.
	 * It may throw a exceptions::cannot_open_file exception if the file cannot be opened.
	 */
	template<class Iter>
	bool write_back(Iter container_begin) const;

	/// write the one-time-step variable data in a container back to the original MM5 model file.
	/**
	 * returns whether successful.
	 * the container should be at least as big as field_size();
	 * and the element arrangement order should be in fortran style.
	 * container_begin is the begin() of the container,
	 * for C's raw array type, it's the address of the array's first element.
	 * It may throw a exceptions::cannot_open_file exception if the file cannot be opened.
	 */
	template<class Iter>
	bool write_back(Iter container_begin, const Infolet& infolet) const;

private:
	Var_Info_Impl *impl_;
};


template<class Iter>
bool Var_Info::fill_data(Iter container_begin) const
{
	Iter current_iter = container_begin;
	for (const_iterator record_iter = begin(); record_iter != end(); ++record_iter)
	{
		std::fstream file(record_iter->filename().c_str(), std::ios_base::in | std::ios_base::binary);
		if (!file)
		{
			throw exceptions::cannot_open_file(record_iter->filename());
		}
		file.seekg(record_iter->field_pos() + std::streamoff(4));
		fortran_real tmp;
		for (int i = 0; i < field_size(); ++i)
		{
			file.read(reinterpret_cast<char*>(&tmp), sizeof(tmp));
#ifndef NO_NEED_TO_SWAP_ENDIAN
			dodomet::swap_endian(tmp);
#endif // NO_NEED_TO_SWAP_ENDIAN
			*current_iter = tmp;
			++current_iter;
		}
	}
	return true;
}

template<class Iter>
bool Var_Info::fill_data(Iter container_begin, const Infolet& infolet) const
{
	Iter current_iter = container_begin;
	for (const_iterator record_iter = begin(); record_iter != end(); ++record_iter)
	{
		if (*record_iter == infolet)
		{
			std::fstream file(record_iter->filename().c_str(), std::ios_base::in | std::ios_base::binary);
			if (!file)
			{
				throw exceptions::cannot_open_file(record_iter->filename());
			}
			file.seekg(record_iter->field_pos() + std::streamoff(4));
			fortran_real tmp;
			for (int i = 0; i < field_size(); ++i)
			{
				file.read(reinterpret_cast<char*>(&tmp), sizeof(tmp));
#ifndef NO_NEED_TO_SWAP_ENDIAN
				dodomet::swap_endian(tmp);
#endif // NO_NEED_TO_SWAP_ENDIAN
				*current_iter = tmp;
				++current_iter;
			}
			return true;
		}
	}
	return false;
}

template<class Iter>
bool Var_Info::write_back(Iter container_begin) const
{
	Iter current_iter = container_begin;
	for (const_iterator record_iter = begin(); record_iter != end(); ++record_iter)
	{
		std::fstream file(record_iter->filename().c_str(), std::ios_base::out | std::ios_base::in | std::ios_base::binary);
		if (!file)
		{
			throw exceptions::cannot_open_file(record_iter->filename());
		}
		file.seekp(record_iter->field_pos() + std::streamoff(4));
		fortran_real tmp;
		for (int i = 0; i < field_size(); ++i)
		{
			tmp = *current_iter;
#ifndef NO_NEED_TO_SWAP_ENDIAN
			dodomet::swap_endian(tmp);
#endif // NO_NEED_TO_SWAP_ENDIAN
			file.write(reinterpret_cast<char*>(&tmp), sizeof(tmp));
			++current_iter;
		}
	}
	return true;
}

template<class Iter>
bool Var_Info::write_back(Iter container_begin, const Infolet& infolet) const
{
	Iter current_iter = container_begin;
	for (const_iterator record_iter = begin(); record_iter != end(); ++record_iter)
	{
		if (*record_iter == infolet)
		{
			std::fstream file(record_iter->filename().c_str(), std::ios_base::out | std::ios_base::in | std::ios_base::binary);
			if (!file)
			{
				throw exceptions::cannot_open_file(record_iter->filename());
			}
			file.seekp(record_iter->field_pos() + std::streamoff(4));
			fortran_real tmp;
			for (int i = 0; i < field_size(); ++i)
			{
				tmp = *current_iter;
#ifndef NO_NEED_TO_SWAP_ENDIAN
				dodomet::swap_endian(tmp);
#endif // NO_NEED_TO_SWAP_ENDIAN
				file.write(reinterpret_cast<char*>(&tmp), sizeof(tmp));
				++current_iter;
			}
			return true;
		}
	}
	return false;
}

template<class UnPred>
void Var_Info_Impl::remove_if(UnPred pred)
{
	field_pos_.remove_if(pred);
}

template<class UnPred>
void Var_Info::remove_if(UnPred pred)
{
	on_write();
	impl_->remove_if(pred);
}

} // namespace dodomet::mm5
} // namespace dodomet

#endif //DODO_VAR_INFO_H

